I dataframe sono delle strutture dati equivalenti alle tabelle di un database.
Al giorno d'oggi tutti i linguaggi di alto livello ne hanno un'implementazione.
Un dataframe è una tabella di dati indicizzata da righe e colonne.
il caso più semplice è una tabella che contanga informazioni a proposito di persone;
Ogni riga contiene un'indicativo unico della persona, ogni colonna rappresenta delle caratteristiche di quella persona.
data
età | altezza | genere | |
---|---|---|---|
nome | |||
Andrea | 24 | 178 | Maschio |
Maria | 33 | 154 | Femmina |
Luca | 30 | 175 | Maschio |
Righe e colonne possono avere indici GERARCHICI, in cui ho più livelli di indicizzazione delle mie informazioni
data
tipologia | domicilio | residenza | |||
---|---|---|---|---|---|
città | indirizzo | città | indirizzo | ||
nome | anno | ||||
Andrea | 2015 | Bologna | via larga 30 | Rimini | via stretta 20 |
2016 | Bologna | via larga 30 | Rimini | via stretta 20 | |
Giulio | 2015 | Bologna | via falsa 40 | Rimini | via giusta 50 |
2016 | Bologna | via torna 10 | Bologna | via torna 10 |
I dataframe permettono di raccogliere e manipolare le informazioni in modo particolarmente comodo, e sono il pilastro centrale della moderna analisi dati.
In particolare hanno una rilevanza centrale i dataframe strutturati come TIDY DATA, termine introdotto da Wickham nel 2010 (paper di spiegazione).
I tidy data sono un modo particolare di strutturare le tabelle che rende l'analisi ed il mantenimento dei dati particolarmente comodo.
La struttura tidy data che Wickham definisce è caratterizzata dalle seguenti proprietà:
Dove:
Corrisponde dal punto di vista formale alla terza forma normale dei database.
data
nome | anno | tipologia | città | indirizzo | |
---|---|---|---|---|---|
0 | Andrea | 2015 | residenza | Rimini | via stretta 20 |
1 | Andrea | 2015 | domicilio | Bologna | via larga 30 |
2 | Andrea | 2016 | residenza | Rimini | via stretta 20 |
3 | Andrea | 2016 | domicilio | Bologna | via larga 30 |
4 | Giulio | 2015 | residenza | Rimini | via giusta 50 |
5 | Giulio | 2015 | domicilio | Bologna | via falsa 40 |
6 | Giulio | 2016 | residenza | Bologna | via torna 10 |
7 | Giulio | 2016 | domicilio | Bologna | via torna 10 |
La cosa importante da ricordare con questo tipo di dati (e la approfondiremo meglio la prossima lezione) è che il formato di dati per fare storage non è necessariamente il più comodo per ogni possibile analisi.
Il formato tidy è eccezionale, sopratutto per mantenere i metadati sulle mie misure, ma non sempre è il formato più comodo per l'analisi che voglio fare (ad esempio differenze fra vari momenti temporali).
Per questo motivo, tutte le librerie che lavorano sui dataframe hanno un forte focus sulla trasformazione dei ati da una forma all'altra, per permetterci di passare facilmente alla struttura dati che serve alle analisi senza sacrificare la qualità dei dati in storage.
Pandas è la libreria di python che permette la manipolazione dei dataframe.
La libreria introduce la classe DataFrame, che contiene una tabella, che contiene diverse Series, ovvero i dati in colonna che condividono lo stesso indice.
import pandas as pd
import numpy as np
Uno dei punti di forza di pandas è la capacità di leggere e scrivere praticamente qualsiasi dato di tipo tabulare.
Ad esempio possiamo caricare tutte le tabelle di una pagina wikipedia
page = 'https://en.wikipedia.org/wiki/List_of_highest-grossing_films'
wikitables = pd.read_html(page, attrs={"class":"wikitable"})
len(wikitables)
83
wikitables[0].head()
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | Rank | Peak | Title | Worldwide gross | Year | Reference(s) |
1 | 1 | 1 | Avatar | $2,787,965,087 | 2009 | [# 1][# 2] |
2 | 2 | 1 | Titanic | $2,186,772,302 | 1997 | [# 3][# 4] |
3 | 3 | 3 | Star Wars: The Force Awakens | $2,068,223,624 | 2015 | [# 5][# 6] |
4 | 4 | 3 | Jurassic World | $1,670,400,637 | 2015 | [# 7][# 8] |
Le funzioni di lettura contengono decine e decine di parametri per riuscire a leggere i dati esattamente come vogliamo.
wikitables = pd.read_html(page, attrs={"class":"wikitable"}, header=0)
wikitables[0].head()
Rank | Peak | Title | Worldwide gross | Year | Reference(s) | |
---|---|---|---|---|---|---|
0 | 1 | 1 | Avatar | $2,787,965,087 | 2009 | [# 1][# 2] |
1 | 2 | 1 | Titanic | $2,186,772,302 | 1997 | [# 3][# 4] |
2 | 3 | 3 | Star Wars: The Force Awakens | $2,068,223,624 | 2015 | [# 5][# 6] |
3 | 4 | 3 | Jurassic World | $1,670,400,637 | 2015 | [# 7][# 8] |
4 | 5 | 3 | The Avengers | $1,518,812,988 | 2012 | [# 9][# 10] |
dataframe = wikitables[0].copy()
Il dataframe si comporta come una specie di dizionario.
Le chiavi sono le colonne, e ciascuna chiave fa riferimento alla Series lì contenuta
dataframe.columns
Index(['Rank', 'Peak', 'Title', 'Worldwide gross', 'Year', 'Reference(s)'], dtype='object')
dataframe['Year'].head()
0 2009 1 1997 2 2015 3 2015 4 2012 Name: Year, dtype: int64
Le Series si comportano come gli array di numpy per quanto riguarda la vettorizzazione, ma lo fanno informati dall'indice della serie invece che dall'ordine degli elementi
dataframe['Year'].head() * 100
0 200900 1 199700 2 201500 3 201500 4 201200 Name: Year, dtype: int64
a = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
a
a 1 b 2 c 3 dtype: int64
b = pd.Series([5, 6, 7], index=['c', 'a', 'b'])
b
c 5 a 6 b 7 dtype: int64
a+b
a 7 b 9 c 8 dtype: int64
posso manipolare le mie colonne in molti modi, partendo dall'eliminazione di colonne non interessanti
del dataframe['Reference(s)']
dataframe.head()
Rank | Peak | Title | Worldwide gross | Year | |
---|---|---|---|---|---|
0 | 1 | 1 | Avatar | $2,787,965,087 | 2009 |
1 | 2 | 1 | Titanic | $2,186,772,302 | 1997 |
2 | 3 | 3 | Star Wars: The Force Awakens | $2,068,223,624 | 2015 |
3 | 4 | 3 | Jurassic World | $1,670,400,637 | 2015 |
4 | 5 | 3 | The Avengers | $1,518,812,988 | 2012 |
dataframe.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 50 entries, 0 to 49 Data columns (total 5 columns): Rank 50 non-null object Peak 50 non-null object Title 50 non-null object Worldwide gross 50 non-null object Year 50 non-null int64 dtypes: int64(1), object(4) memory usage: 2.0+ KB
dataframe['Title'].head()
0 Avatar 1 Titanic 2 Star Wars: The Force Awakens 3 Jurassic World 4 The Avengers Name: Title, dtype: object
dataframe.head()
Rank | Peak | Title | Worldwide gross | Year | |
---|---|---|---|---|---|
0 | 1 | 1 | Avatar | $2,787,965,087 | 2009 |
1 | 2 | 1 | Titanic | $2,186,772,302 | 1997 |
2 | 3 | 3 | Star Wars: The Force Awakens | $2,068,223,624 | 2015 |
3 | 4 | 3 | Jurassic World | $1,670,400,637 | 2015 |
4 | 5 | 3 | The Avengers | $1,518,812,988 | 2012 |
Spesso e volentieri i dati reali arrivano in un formato "non ottimale".
La prima parte di qualsiasi analisi consiste nella pulizia dei dati.
Nel nostro caso ad esempio potremmo voler aggiustare il guadagno, che al momento è visto come una stringa.
c = 'Worldwide gross'
dataframe[c] = dataframe[c].str.replace(',', '')
dataframe[c] = dataframe[c].str.replace('$', '')
dataframe[c] = dataframe[c].astype(int)
dataframe.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 50 entries, 0 to 49 Data columns (total 5 columns): Rank 50 non-null object Peak 50 non-null object Title 50 non-null object Worldwide gross 50 non-null int64 Year 50 non-null int64 dtypes: int64(2), object(3) memory usage: 2.0+ KB
Per aggiustare i dati talvolta è necessario usare la violenza...
try:
dataframe['Peak'].astype(int)
except ValueError as e:
print(e)
invalid literal for int() with base 10: '4TS3'
print(list(dataframe['Peak'].unique()))
['1', '3', '4', '5', '10', '12', '2', '7', '4TS3', '20', '6', '22', '24', '14', '19DM2', '30', '26', '8FN', '8', '15', '40', '29', '47', '45']
non esiste una trasformazione semplice che possa convertire questo tipo di dati, quindi usiamo una regular expression
import re
regex = re.compile('(\d+)\D*\d*')
dataframe['Peak'] = dataframe['Peak'].str.extract(regex, expand=False)
dataframe['Peak'] = dataframe['Peak'].astype(int)
dataframe.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 50 entries, 0 to 49 Data columns (total 5 columns): Rank 50 non-null object Peak 50 non-null int64 Title 50 non-null object Worldwide gross 50 non-null int64 Year 50 non-null int64 dtypes: int64(3), object(2) memory usage: 2.0+ KB
dataframe['Rank'] = dataframe['Rank'].str.extract(regex, expand=False)
dataframe['Rank'] = dataframe['Rank'].astype(int)
dataframe.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 50 entries, 0 to 49 Data columns (total 5 columns): Rank 50 non-null int64 Peak 50 non-null int64 Title 50 non-null object Worldwide gross 50 non-null int64 Year 50 non-null int64 dtypes: int64(4), object(1) memory usage: 2.0+ KB
dataframe.describe()
Rank | Peak | Worldwide gross | Year | |
---|---|---|---|---|
count | 50.00000 | 50.000000 | 5.000000e+01 | 50.00000 |
mean | 25.50000 | 10.900000 | 1.131099e+09 | 2009.42000 |
std | 14.57738 | 11.429018 | 3.661334e+08 | 6.07803 |
min | 1.00000 | 1.000000 | 8.719397e+08 | 1993.00000 |
25% | 13.25000 | 4.000000 | 9.399983e+08 | 2006.25000 |
50% | 25.50000 | 6.000000 | 1.024626e+09 | 2011.00000 |
75% | 37.75000 | 13.500000 | 1.122905e+09 | 2014.75000 |
max | 50.00000 | 47.000000 | 2.787965e+09 | 2016.00000 |
Posso farlo sia da Pandas che da Matplotlib.
Dopo vedremo una libreria più appropriata, ma queste vanno bene per un approccio quick & dirty
import pylab as plt
plt.scatter('Rank', 'Peak', data=dataframe)
<matplotlib.collections.PathCollection at 0x7f5ce8b80828>
dataframe.plot.scatter('Rank', 'Peak')
<matplotlib.axes._subplots.AxesSubplot at 0x7f5cac89aa20>
plt.plot('Rank', 'Worldwide gross', data=dataframe)
[<matplotlib.lines.Line2D at 0x7f5cacb616d8>]
with plt.xkcd():
dataframe.plot('Rank', 'Worldwide gross')
plt.hist('Worldwide gross', data=dataframe);
dataframe['Worldwide gross'].plot.hist()
<matplotlib.axes._subplots.AxesSubplot at 0x7f5cacc9a0f0>
pandas lavora in realtà con matplotlib, per cui potete estrarre il grafico al volo e modificarlo come volete
dataframe['Worldwide gross'].plot.hist()
ax = plt.gca()
ax.set_title("Histogram of Worldwide gross", fontsize=30)
<matplotlib.text.Text at 0x7f5caca29c88>
I dataframe di Pandas (ed in generale la struttura del dataframe come concetto), contiene come dati in colonna soltanto dei valori scalari, come l'altezza, il peso, e così via.
Per gestire e manipolare dati più complessi, come ad esempio una serie temporale per ogni paziente, si devono spesso creare più tabelle e collegarle logicamente fra di loro.
Una alternativa piuttosto recente sono i DataArray e DataSet delle libreria XArray.
Questi permettono di creare delle strutture molto efficienti per gestire ad esempio immagini TAC dei pazienti in più dimensioni ed in più momenti temporali e manipolarle in modo sensato (per quanto questo possa essere possibile).
Vediamo ora una serie di operazioni molto comuni sui dataframe.
Compie operazioni di tipo split-apply-combine:
wiki = "https://it.wikipedia.org/wiki/"
url_popolazione = wiki+"Comuni_d%27Italia_per_popolazione"
url_superficie = wiki+"Primi_100_comuni_italiani_per_superficie"
comuni_popolazione = pd.read_html(url_popolazione,
attrs={"class":"wikitable"},
header=0)
comuni_popolazione = comuni_popolazione[0]
comuni_popolazione.head()
# | Comune | Regione | Provincia / Città metropolitana | Abitanti | |
---|---|---|---|---|---|
0 | 1 | Roma | Lazio | Roma | 2 867 078 |
1 | 2 | Milano | Lombardia | Milano | 1 350 487 |
2 | 3 | Napoli | Campania | Napoli | 972 212 |
3 | 4 | Torino | Piemonte | Torino | 889 600 |
4 | 5 | Palermo | Sicilia | Palermo | 671 531 |
comuni_superficie = pd.read_html(url_superficie,
attrs={"class":"wikitable"},
header=0)
comuni_superficie = comuni_superficie[0]
comuni_superficie.head()
Pos. | Comune | Regione | Provincia | Superficie (km²) | |
---|---|---|---|---|---|
0 | 1 | Roma | Lazio | Roma | 128736 |
1 | 2 | Ravenna | Emilia-Romagna | Ravenna | 65382 |
2 | 3 | Cerignola | Puglia | Foggia | 59393 |
3 | 4 | Noto | Sicilia | Siracusa | 55499 |
4 | 5 | Sassari | Sardegna | Sassari | 54704 |
comuni_superficie.groupby('Regione').mean()
Pos. | Superficie (km²) | |
---|---|---|
Regione | ||
Abruzzo | 9.000000 | 47391.000000 |
Basilicata | 54.000000 | 29641.000000 |
Calabria | 63.000000 | 26078.500000 |
Emilia-Romagna | 55.400000 | 31154.100000 |
Lazio | 28.750000 | 56263.750000 |
Liguria | 79.000000 | 24029.000000 |
Lombardia | 93.500000 | 22701.500000 |
Marche | 83.333333 | 24168.000000 |
Puglia | 45.526316 | 33032.631579 |
Sardegna | 66.111111 | 29028.333333 |
Sicilia | 45.600000 | 32300.150000 |
Toscana | 51.357143 | 29941.357143 |
Trentino-Alto Adige | 55.000000 | 27485.000000 |
Umbria | 31.714286 | 36175.285714 |
Veneto | 48.333333 | 30853.000000 |
g = comuni_superficie.groupby('Regione')
g.aggregate([np.mean, np.std, pd.Series.count])
Pos. | Superficie (km²) | |||||
---|---|---|---|---|---|---|
mean | std | count | mean | std | count | |
Regione | ||||||
Abruzzo | 9.000000 | NaN | 1 | 47391.000000 | NaN | 1 |
Basilicata | 54.000000 | 32.787193 | 3 | 29641.000000 | 8419.030110 | 3 |
Calabria | 63.000000 | 24.041631 | 2 | 26078.500000 | 3075.207391 | 2 |
Emilia-Romagna | 55.400000 | 30.492986 | 10 | 31154.100000 | 13146.217719 | 10 |
Lazio | 28.750000 | 24.185050 | 4 | 56263.750000 | 48688.754926 | 4 |
Liguria | 79.000000 | NaN | 1 | 24029.000000 | NaN | 1 |
Lombardia | 93.500000 | 2.121320 | 2 | 22701.500000 | 40.305087 | 2 |
Marche | 83.333333 | 24.542480 | 3 | 24168.000000 | 2632.717987 | 3 |
Puglia | 45.526316 | 29.279057 | 19 | 33032.631579 | 10050.061521 | 19 |
Sardegna | 66.111111 | 32.811753 | 9 | 29028.333333 | 10824.258624 | 9 |
Sicilia | 45.600000 | 27.013447 | 20 | 32300.150000 | 9656.570331 | 20 |
Toscana | 51.357143 | 26.304744 | 14 | 29941.357143 | 7114.648125 | 14 |
Trentino-Alto Adige | 55.000000 | 24.041631 | 2 | 27485.000000 | 3877.773588 | 2 |
Umbria | 31.714286 | 20.361027 | 7 | 36175.285714 | 9897.267396 | 7 |
Veneto | 48.333333 | 28.884829 | 3 | 30853.000000 | 9300.741315 | 3 |
comuni_superficie.groupby('Regione')['Superficie (km²)'].count()
Regione Abruzzo 1 Basilicata 3 Calabria 2 Emilia-Romagna 10 Lazio 4 Liguria 1 Lombardia 2 Marche 3 Puglia 19 Sardegna 9 Sicilia 20 Toscana 14 Trentino-Alto Adige 2 Umbria 7 Veneto 3 Name: Superficie (km²), dtype: int64
g = comuni_superficie.groupby('Regione')['Superficie (km²)']
g.count().sort_values(ascending=False)
Regione Sicilia 20 Puglia 19 Toscana 14 Emilia-Romagna 10 Sardegna 9 Umbria 7 Lazio 4 Veneto 3 Marche 3 Basilicata 3 Trentino-Alto Adige 2 Lombardia 2 Calabria 2 Liguria 1 Abruzzo 1 Name: Superficie (km²), dtype: int64
g = comuni_popolazione.groupby('Regione')['Abitanti']
g.count().sort_values(ascending=False)
Regione Campania 19 Lombardia 15 Sicilia 15 Puglia 15 Toscana 13 Emilia-Romagna 13 Lazio 11 Veneto 6 Piemonte 6 Calabria 5 Abruzzo 5 Liguria 4 Sardegna 4 Marche 3 Umbria 3 Friuli-Venezia Giulia 3 Trentino-Alto Adige 2 Basilicata 2 Name: Abitanti, dtype: int64
Quando ho due tabelle distinte, che condividono una chiave, posso fare il join fra le due.
Questo mi permette di tenere le tabelle dei miei dati in forma corretta (tidy e scorrelata) e ricrearle in modo comodo per le analisi combinando più tabelle insieme
a
nome | genere | |
---|---|---|
0 | Antonio | M |
1 | Marco | M |
2 | Francesca | F |
b
nome | spesa | |
---|---|---|
0 | Antonio | 15 |
1 | Marco | 10 |
2 | Marco | 12 |
3 | Marco | 23 |
4 | Francesca | 20 |
pd.merge(a, b, on='nome')
nome | genere | spesa | |
---|---|---|---|
0 | Antonio | M | 15 |
1 | Marco | M | 10 |
2 | Marco | M | 12 |
3 | Marco | M | 23 |
4 | Francesca | F | 20 |
(
pd.merge(comuni_popolazione,
comuni_superficie,
on=['Comune', 'Regione'])
).head()
# | Comune | Regione | Provincia / Città metropolitana | Abitanti | Pos. | Provincia | Superficie (km²) | |
---|---|---|---|---|---|---|---|---|
0 | 1 | Roma | Lazio | Roma | 2 867 078 | 1 | Roma | 128736 |
1 | 6 | Genova | Liguria | Genova | 585 208 | 79 | Genova | 24029 |
2 | 11 | Venezia | Veneto | Venezia | 262 344 | 15 | Venezia | 41590 |
3 | 16 | Taranto | Puglia | Taranto | 200 385 | 70 | Taranto | 24986 |
4 | 18 | Parma | Emilia-Romagna | Parma | 194 152 | 61 | Parma | 26060 |
Ci sono diversi modi di fare il merge, che corrispondono alle diverse combinazioni di insiemi possibili di chiavi.
sono controllati dal parametro HOW.
len(pd.merge(comuni_popolazione, comuni_superficie,
on='Comune', how='right'))
100
len(pd.merge(comuni_popolazione, comuni_superficie,
on='Comune', how='left'))
144
len(pd.merge(comuni_popolazione, comuni_superficie,
on='Comune', how='inner'))
38
len(pd.merge(comuni_popolazione, comuni_superficie,
on='Comune', how='outer'))
206
il pivoting permette di creare tavole riassuntive (incluse tavole di contingenza) a partire da una dataset tidy.
date due colonne che faranno da indice e nomi delle colonne del dataset risultante, si scelgono uno o più valori di cui fare il sommario (somma, media, conteggi, deviazione standard, etc...)
spese = [('Antonio', 'gatto', 4),
('Antonio', 'gatto', 5),
('Antonio', 'gatto', 6),
('Giulia', 'gatto', 3),
('Giulia', 'cane', 7),
('Giulia', 'cane', 8),
]
spese = pd.DataFrame(spese, columns = ['nome', 'animale', 'spesa'])
spese
nome | animale | spesa | |
---|---|---|---|
0 | Antonio | gatto | 4 |
1 | Antonio | gatto | 5 |
2 | Antonio | gatto | 6 |
3 | Giulia | gatto | 3 |
4 | Giulia | cane | 7 |
5 | Giulia | cane | 8 |
pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=np.sum)
animale | cane | gatto |
---|---|---|
nome | ||
Antonio | NaN | 15.0 |
Giulia | 15.0 | 3.0 |
pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=np.sum,
fill_value=0)
animale | cane | gatto |
---|---|---|
nome | ||
Antonio | 0 | 15 |
Giulia | 15 | 3 |
pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=np.sum,
fill_value=0,
margins=True)
animale | cane | gatto | All |
---|---|---|---|
nome | |||
Antonio | 0.0 | 15.0 | 15.0 |
Giulia | 15.0 | 3.0 | 18.0 |
All | 15.0 | 18.0 | 33.0 |
pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=pd.Series.count,
fill_value=0)
animale | cane | gatto |
---|---|---|
nome | ||
Antonio | 0 | 3 |
Giulia | 2 | 1 |
r = pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=pd.Series.count,
fill_value=0)
r = r.reset_index()
r
animale | nome | cane | gatto |
---|---|---|---|
0 | Antonio | 0 | 3 |
1 | Giulia | 2 | 1 |
v = pd.melt(r, id_vars=['nome'], value_vars=['cane', 'gatto'])
v
nome | animale | value | |
---|---|---|---|
0 | Antonio | cane | 0 |
1 | Giulia | cane | 2 |
2 | Antonio | gatto | 3 |
3 | Giulia | gatto | 1 |
v2 = v.set_index(['nome', 'animale'])['value']
v2
nome animale Antonio cane 0 Giulia cane 2 Antonio gatto 3 Giulia gatto 1 Name: value, dtype: int64
v2.unstack()
animale | cane | gatto |
---|---|---|
nome | ||
Antonio | 0 | 3 |
Giulia | 2 | 1 |
v2.unstack().stack()
nome animale Antonio cane 0 gatto 3 Giulia cane 2 gatto 1 dtype: int64
v.pivot(index='nome', columns='animale', values='value')
animale | cane | gatto |
---|---|---|
nome | ||
Antonio | 0 | 3 |
Giulia | 2 | 1 |
v.pivot(index='nome', columns='animale', values='value')
è identico a
v2.unstack()
ma uno agisce sulle serie (unstack) e l'altro sui dataframe di tipo tidy (pivot)
url_amminoacidi = 'https://en.wikipedia.org/wiki/Proteinogenic_amino_acid'
info_amminoacidi = pd.read_html(url_amminoacidi, attrs={"class":"wikitable"}, header=0)
len(info_amminoacidi)
6
info_amminoacidi[0].head()
Amino acid | Short | Abbrev. | Avg. mass (Da) | pI | pK1 (α-COOH) | pK2 (α-+NH3) | |
---|---|---|---|---|---|---|---|
0 | Alanine | A | Ala | 89.09404 | 6.01 | 2.35 | 9.87 |
1 | Cysteine | C | Cys | 121.15404 | 5.05 | 1.92 | 10.70 |
2 | Aspartic acid | D | Asp | 133.10384 | 2.85 | 1.99 | 9.90 |
3 | Glutamic acid | E | Glu | 147.13074 | 3.15 | 2.10 | 9.47 |
4 | Phenylalanine | F | Phe | 165.19184 | 5.49 | 2.20 | 9.31 |
info_amminoacidi[1].head()
Amino acid | Short | Abbrev. | Side chain | Hydro- phobic | pKa§ | Polar | pH | Small | Tiny | Aromatic or Aliphatic | van der Waals volume (Å3) | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Alanine | A | Ala | -CH3 | X | - | - | - | X | X | Aliphatic | 67.0 |
1 | Cysteine | C | Cys | -CH2SH | X | 8.55 | - | acidic | X | X | - | 86.0 |
2 | Aspartic acid | D | Asp | -CH2COOH | - | 3.67 | X | acidic | X | - | - | 91.0 |
3 | Glutamic acid | E | Glu | -CH2CH2COOH | - | 4.25 | X | acidic | - | - | - | 109.0 |
4 | Phenylalanine | F | Phe | -CH2C6H5 | X | - | - | - | - | - | Aromatic | 135.0 |
tavola1 = info_amminoacidi[0]
tavola2 = info_amminoacidi[1]
del tavola2['Short']
del tavola2['Abbrev.']
tavola = pd.merge(tavola1, tavola2, on='Amino acid')
tavola.head()
Amino acid | Short | Abbrev. | Avg. mass (Da) | pI | pK1 (α-COOH) | pK2 (α-+NH3) | Side chain | Hydro- phobic | pKa§ | Polar | pH | Small | Tiny | Aromatic or Aliphatic | van der Waals volume (Å3) | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Alanine | A | Ala | 89.09404 | 6.01 | 2.35 | 9.87 | -CH3 | X | - | - | - | X | X | Aliphatic | 67.0 |
1 | Cysteine | C | Cys | 121.15404 | 5.05 | 1.92 | 10.70 | -CH2SH | X | 8.55 | - | acidic | X | X | - | 86.0 |
2 | Aspartic acid | D | Asp | 133.10384 | 2.85 | 1.99 | 9.90 | -CH2COOH | - | 3.67 | X | acidic | X | - | - | 91.0 |
3 | Glutamic acid | E | Glu | 147.13074 | 3.15 | 2.10 | 9.47 | -CH2CH2COOH | - | 4.25 | X | acidic | - | - | - | 109.0 |
4 | Phenylalanine | F | Phe | 165.19184 | 5.49 | 2.20 | 9.31 | -CH2C6H5 | X | - | - | - | - | - | Aromatic | 135.0 |
tavola.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 22 entries, 0 to 21 Data columns (total 16 columns): Amino acid 22 non-null object Short 22 non-null object Abbrev. 22 non-null object Avg. mass (Da) 22 non-null float64 pI 21 non-null float64 pK1 (α-COOH) 21 non-null float64 pK2 (α-+NH3) 21 non-null float64 Side chain 22 non-null object Hydro- phobic 22 non-null object pKa§ 22 non-null object Polar 22 non-null object pH 22 non-null object Small 22 non-null object Tiny 22 non-null object Aromatic or Aliphatic 22 non-null object van der Waals volume (Å3) 20 non-null float64 dtypes: float64(5), object(11) memory usage: 2.9+ KB
tavola['Hydro- phobic'].unique()
array(['X', '-'], dtype=object)
import seaborn
Seaborn di default cambia la configurazione stardard della visualizzazione di matplotlib.
Potete impostarla come preferite grazie al modulo styles di matplotlib.
from matplotlib import style
print(sorted(style.available))
style.use('default')
['bmh', 'classic', 'dark_background', 'fivethirtyeight', 'ggplot', 'grayscale', 'seaborn', 'seaborn-bright', 'seaborn-colorblind', 'seaborn-dark', 'seaborn-dark-palette', 'seaborn-darkgrid', 'seaborn-deep', 'seaborn-muted', 'seaborn-notebook', 'seaborn-paper', 'seaborn-pastel', 'seaborn-poster', 'seaborn-talk', 'seaborn-ticks', 'seaborn-white', 'seaborn-whitegrid']
seaborn.lmplot('Avg. mass (Da)',
'van der Waals volume (Å3)',
data=tavola,
hue='Hydro- phobic')
<seaborn.axisgrid.FacetGrid at 0x7f3c4bc0fa58>
seaborn.lmplot('Avg. mass (Da)',
'van der Waals volume (Å3)',
data=tavola,
col='Hydro- phobic')
<seaborn.axisgrid.FacetGrid at 0x7f3c4b5991d0>
seaborn.lmplot('Avg. mass (Da)',
'van der Waals volume (Å3)',
data=tavola,
row='Hydro- phobic')
<seaborn.axisgrid.FacetGrid at 0x7f3c496f1c18>
fg = seaborn.FacetGrid(data=tavola,
hue='Hydro- phobic',
size=6)
fg.map(plt.scatter,
'Avg. mass (Da)',
'van der Waals volume (Å3)',
s=50)
fg.map(seaborn.regplot,
'Avg. mass (Da)',
'van der Waals volume (Å3)',
scatter=False)
fg.add_legend();
fg = seaborn.FacetGrid(data=tavola,
col='Hydro- phobic',
hue='Hydro- phobic',
size=8,
aspect=.5)
fg.map(plt.scatter,
'Avg. mass (Da)',
'van der Waals volume (Å3)',
s=50)
fg.map(seaborn.regplot,
'Avg. mass (Da)',
'van der Waals volume (Å3)',
scatter=False)
fg.add_legend();
columns = ['Avg. mass (Da)',
'van der Waals volume (Å3)',
'pI', 'pK1 (α-COOH)',
'pK2 (α-+NH3)']
tavola[columns].head()
Avg. mass (Da) | van der Waals volume (Å3) | pI | pK1 (α-COOH) | pK2 (α-+NH3) | |
---|---|---|---|---|---|
0 | 89.09404 | 67.0 | 6.01 | 2.35 | 9.87 |
1 | 121.15404 | 86.0 | 5.05 | 1.92 | 10.70 |
2 | 133.10384 | 91.0 | 2.85 | 1.99 | 9.90 |
3 | 147.13074 | 109.0 | 3.15 | 2.10 | 9.47 |
4 | 165.19184 | 135.0 | 5.49 | 2.20 | 9.31 |
fd = tavola[columns+['Hydro- phobic']].dropna()
g = seaborn.PairGrid(df,
size=2,
hue='Hydro- phobic')
g.map_diag(plt.hist, alpha=0.5)
g.map_offdiag(plt.scatter, alpha=0.75, s=20);
Esaminando la tabella di wikipedia dei premi nobel in fisica, classificare quali paese abbiamo avuto il maggior numero di nobel nel corso degli anni
Per i più coraggiosi, provate a correlare la lista precendente con il consumo pro capite di birra.
Purtroppo mi sono accorto solo a posteriori che la tabella dei nobel aveva delle anomalie:
Il secondo problema si può risolvere facilmente con la decisione arbitraria di conteggiare soltanto la prima cittadinanza (la nazione di nascita, secondo quanto riportato dal sito del premio nobel).
Il primo richiede la manipolazione delle righe del dataframe, che mostro di seguito.
Come vedrete la manipolazione non è particolarmente complicata, ma richiede conoscenze che non vi ho mostrato a lezione, mi spiace.
# carico il dataset incriminato
import pandas as pd
wiki = 'https://en.wikipedia.org/wiki/'
url = wiki+'List_of_Nobel_laureates_in_Physics'
dfs = pd.read_html(url,
attrs={"class":"wikitable"},
header=0)
df = dfs[0].copy()
df.head()
Year | Laureate[A] | Country[B] | Rationale[C] | Unnamed: 4 | |
---|---|---|---|---|---|
0 | 1901.0 | NaN | Wilhelm Conrad Röntgen | Germany | "in recognition of the extraordinary services ... |
1 | 1902.0 | NaN | Hendrik Lorentz | Netherlands | "in recognition of the extraordinary service t... |
2 | NaN | Pieter Zeeman | Netherlands | NaN | NaN |
3 | 1903.0 | NaN | Antoine Henri Becquerel | France | "for his discovery of spontaneous radioactivit... |
4 | NaN | Pierre Curie | France | "for their joint researches on the radiation p... | NaN |
# L'anno ha dei buchi dove ci sono più vincitori
# posso riempire i buchi conuna funzione 'forward-fill'
# che li riempe con il valore immediatamente precedente
df['Year'] = y.fillna(method='ffill').astype(int)
df.head()
Year | Laureate[A] | Country[B] | Rationale[C] | Unnamed: 4 | |
---|---|---|---|---|---|
0 | 1901 | NaN | Wilhelm Conrad Röntgen | Germany | "in recognition of the extraordinary services ... |
1 | 1902 | NaN | Hendrik Lorentz | Netherlands | "in recognition of the extraordinary service t... |
2 | 1902 | Pieter Zeeman | Netherlands | NaN | NaN |
3 | 1903 | NaN | Antoine Henri Becquerel | France | "for his discovery of spontaneous radioactivit... |
4 | 1903 | Pierre Curie | France | "for their joint researches on the radiation p... | NaN |
# la parte difficile.
# per prima cosa devo fare la correzione una riga alla volta
for idx, line in df.iterrows():
# se il vincitore di quella riga è assente
# devo fare la correzione
missing_winner = pd.isnull(line['Laureate[A]'])
if missing_winner:
# creo una nuova tupla di valori ricombinando
# quella vecchia nell'ordine corretto
new_value = line[['Year', 'Country[B]', 'Rationale[C]']]
# riempo le prime 3 colonne con i valori che ho trovato
# l'argomento values mi serve a fargli ignorare
# l'indicizzazione esplicita della serie
df.loc[idx, :3] = new_value.values
df.head()
Year | Laureate[A] | Country[B] | Rationale[C] | Unnamed: 4 | |
---|---|---|---|---|---|
0 | 1901 | Wilhelm Conrad Röntgen | Germany | Germany | "in recognition of the extraordinary services ... |
1 | 1902 | Hendrik Lorentz | Netherlands | Netherlands | "in recognition of the extraordinary service t... |
2 | 1902 | Pieter Zeeman | Netherlands | NaN | NaN |
3 | 1903 | Antoine Henri Becquerel | France | France | "for his discovery of spontaneous radioactivit... |
4 | 1903 | Pierre Curie | France | "for their joint researches on the radiation p... | NaN |
# rendiamo il dataframe un po' più piacevole allo sguardo
del df['Rationale[C]']
del df['Unnamed: 4']
df.columns = ['year', 'name', 'country']
df.head()
year | name | country | |
---|---|---|---|
0 | 1901 | Wilhelm Conrad Röntgen | Germany |
1 | 1902 | Hendrik Lorentz | Netherlands |
2 | 1902 | Pieter Zeeman | Netherlands |
3 | 1903 | Antoine Henri Becquerel | France |
4 | 1903 | Pierre Curie | France |
# controlliamo i valori unici della variabile country.
# possiamo vedere che dove c'è la doppia cittadinanza
# abbiamo un carattere speciale ('\xa0')
# che possiamo usare per dividerle
df['country'].unique()
array(['Germany', 'Netherlands', 'France', 'Poland \xa0France', 'United Kingdom', 'Austria-Hungary \xa0Germany', 'United States \xa0Poland', 'Italy', 'Sweden', 'Australia \xa0United Kingdom', nan, 'Switzerland', 'Germany Switzerland', 'Denmark', 'United States', 'India', 'Austria', 'Japan', 'Ireland', 'Switzerland \xa0United States', 'West Germany', 'United States \xa0Germany', 'China \xa0United States', 'Soviet Union', 'Hungary \xa0United States', 'Hungary \xa0United Kingdom', 'United States \xa0Norway', 'Pakistan', 'Netherlands \xa0United States', 'India \xa0United States', 'Canada', 'France \xa0Poland', 'Russia', 'Italy \xa0United States', 'Russia \xa0United States', 'United Kingdom \xa0United States', 'Japan \xa0United States', 'Hong Kong \xa0United Kingdom \xa0United States', 'Canada \xa0United States', 'Russia \xa0United Kingdom \xa0Netherlands', 'Russia \xa0United Kingdom', 'Australia \xa0United States', 'Belgium'], dtype=object)
# divido la stringa in due dove trovo il carattere speciale
t = df['country'].str.split('\xa0')
# prendo il primo pezzo di stringa
t = t.str[0]
# tolgo gli spazi bianchi
t = t.str.strip()
# la assegno di nuovo alla variabile country
df['country'] = t
# alcuni anni non hanno il country
# sono quelli in cui il nobel
# non è stato assegnato per via delle guerre
df.iloc[19:22]
year | name | country | |
---|---|---|---|
19 | 1915 | William Lawrence Bragg | Australia |
20 | 1916 | Not awarded World War I | NaN |
21 | 1917 | Charles Glover Barkla | United Kingdom |
# posso rimuovere le linee in cui il country è assente
df = df.dropna(subset=['country'])
# posso infine fare il mio groupby
# e vedere il risultato ordinato
gb = df.groupby('country')['year']
gb.count().sort_values(ascending=False)
country United States 76 United Kingdom 22 Germany 14 France 12 Japan 11 West Germany 9 Netherlands 9 Soviet Union 7 Russia 5 Italy 5 Switzerland 4 Sweden 4 Canada 4 Austria 3 China 3 Denmark 3 India 2 Hungary 2 Australia 2 Germany Switzerland 1 Hong Kong 1 Ireland 1 Pakistan 1 Belgium 1 Austria-Hungary 1 Poland 1 Name: year, dtype: int64
Anche se l'esercizio era più difficile del previsto, sappiate che queste situazione capitano più spesso del previsto.
Ogni volta che avrete dei dati "curati" a mano, preparatevi a trovare ogni qual sorta di strani errori ed inconsistenze, che dovrete raddrizzare a colpi di codice.
In queste cose, Pandas è insostituibile (e questa è la ragione principale del suo successo)